#ifndef BRAD_IS_AWESOME
#define BRAD_IS_AWESOME

#include <stdint.h>
#include "sdisk.h"
#include "sector.h"
#include "bitmap.h"
#include "directory.h"
#include "file.h"

//----------------------Defines---------------------------------

#define MAX_FILES 1000
#define MAX_DIRECTORIES 1000
#define HEADER_NUMSECTORS (sizeof(FileSystemHeader) / SD_SECTORSIZE + (sizeof(FileSystemHeader) % SD_SECTORSIZE > 0))
#define ROOT_DIRECTORY_INDEX 0
#define MAX_NAME_LENGTH 16

//----------------------Structs---------------------------------

typedef uint16_t file_index_t;

typedef struct {
	Bitmask bitmask;
	Directory directories[MAX_DIRECTORIES];
	File files[MAX_FILES];
} FileSystemHeader;

typedef struct {
	File * file;
    void * file_data;
	uint32_t stream_pointer;
} OpenFileHandle;

//---------------------Private Functions--------------------------

static int fs_validName( const FileSystemHeader * header, directory_index_t parent, const char * fileName )
{
    size_t i;
    for ( i = 0; i < MAX_DIRECTORIES; i++ )
    {
        if ( header->directories[i].parentDirectory == parent && strcmp( header->directories[i].directoryName, fileName ) == 0 )
            return -1;
    }

    for ( i = 0; i < MAX_FILES; i++ )
    {
        if ( header->files[i].parentDirectory == parent && strcmp( header->files[i].fileName, fileName ) == 0 )
            return -1;
    }

    return 0;
}

static int fs_findDir( const FileSystemHeader * header, const char * directoryName, directory_index_t parent, directory_index_t * out )
{
	directory_index_t i;
	for ( i = 0; i < MAX_DIRECTORIES; i++ )
	{
		const Directory * dir = header->directories + i;

		if ( dir->parentDirectory == parent && strcmp( dir->directoryName, directoryName ) == 0 )
			break;
	}

    if ( i == MAX_DIRECTORIES )
        return -1;

    *out = i;
	return 0;
}

static int fs_tryMakeFile( FileSystemHeader * header, const char * fileName, directory_index_t parent, File ** out )
{
	//Ensure parent is valid and initialized
	if ( parent >= MAX_DIRECTORIES || directory_isEmpty( header->directories + parent ) )
		return -1;

    if ( fs_validName( header, parent, fileName ) != 0 )
        return -1;

	size_t i;
	for ( i = 0; i < MAX_FILES; i++ )
	{
		if ( file_isEmpty( header->files + i ) )
			break;
	}

	//Search failed, at max num of files.
	if ( i == MAX_FILES )
		return -1;

	if ( file_init( header->files + i, fileName, parent ) != 0)
		return -1;

	*out = header->files + i;
	return 0;
}

static int fs_tryMakeDir( FileSystemHeader * out, const char * directoryName, directory_index_t parent )
{
	//Ensure that parent is valid and initialized
	if ( parent >= MAX_DIRECTORIES || directory_isEmpty( out->directories + parent ) )
		return -1;

    if ( fs_validName( out, parent, directoryName ) != 0 )
        return -1;

	//Find location to store directory information
	directory_index_t i;
	for ( i = 0; i < MAX_DIRECTORIES; i++ )
	{
		if ( directory_isEmpty( out->directories + i ) )
			break;
	}

	//Search failed, at max num of directories.
	if ( i == MAX_DIRECTORIES )
		return -1;

	//Initialie directory
	return directory_init( out->directories + i, directoryName, parent );
}

static void fs_flushHeaderToDisk( FileSystemHeader * header )
{
    size_t i;

    for ( i = 0; i < HEADER_NUMSECTORS; i++ )
    {
        SD_write( i, (char *)header + i * SD_SECTORSIZE );
    }
}

//----------------------Functions---------------------------------

static void fs_init( FileSystemHeader * out )
{
	bitmask_free( &out->bitmask );

	//Init all directories to empty except for root directory
	size_t i;
	directory_init( out->directories + ROOT_DIRECTORY_INDEX, "[root]", ROOT_DIRECTORY_INDEX );
	for ( i = 0; i < MAX_DIRECTORIES; i++ ) {
		if ( i != ROOT_DIRECTORY_INDEX )
			directory_initEmpty( out->directories + i );
	}

	//Init all files to empty
	for ( i = 0; i < MAX_FILES; i++ )
		file_initEmpty( out->files + i );

	//Mark off sectors used by header as in use
	for ( i = 0; i < HEADER_NUMSECTORS; i++ )
		bitmask_flip( &out->bitmask, i );

    fs_flushHeaderToDisk( out );
}

//Get directory_index_t for a directory with a name directoryName with a parent of current
static int fs_getDir( const FileSystemHeader * header, const char * directoryName, directory_index_t current, directory_index_t * out )
{
    if ( strlen( directoryName ) == 0 )
        return -1;

    if ( directoryName[0] == '/' )
        current = ROOT_DIRECTORY_INDEX;

	char * temp = strdup( directoryName );
	char * pch = strtok( temp, "/" );

	while ( pch != NULL ) {
	    if ( strlen( pch ) > MAX_NAME_LENGTH ) {
            free( temp );
            return -1;
	    }

		if ( strcmp( ".", pch ) != 0 ) {
			if ( strcmp( "..", pch ) == 0 )
				current = header->directories[current].parentDirectory;
			else if ( fs_findDir( header, pch, current, &current ) != 0 ) {
                free( temp );
				return -1;
			}
		}

		pch = strtok( NULL, "/" );
	}

	*out = current;
	free( temp );
	return 0;
}

static int fs_getDirAndName( const FileSystemHeader * header, const char * pathName, directory_index_t current, directory_index_t * out_dir, char * out )
{
    if ( strlen( pathName ) == 0 )
        return -1;

    char * temp = strdup(pathName);
    char * ptr = strrchr(temp,'/');

    if ( ptr == NULL )
    {
        if ( strlen( pathName ) > MAX_NAME_LENGTH )
            goto Fail;
        memcpy( out, pathName, MAX_NAME_LENGTH + 1 );
        *out_dir = current;
    } else {
        int len = strlen( ptr );
        if ( len <= 1 || len > MAX_NAME_LENGTH + 1 )
            goto Fail;
        *ptr = '\0';

        ptr++;

        directory_index_t temp_dir;
        if ( fs_getDir( header, temp, current, &temp_dir ) != 0 )
            goto Fail;

        *out_dir = temp_dir;
        memcpy( out, ptr, MAX_NAME_LENGTH + 1 );
    }

    free( temp );
    return 0;

    Fail:
        free( temp );
        return -1;
}

//Returns a openfilehandle.  WARNING: Does not check if file is already open.
static int fs_tryOpenFile( FileSystemHeader * header, const char * fileName, directory_index_t current, OpenFileHandle * out )
{
	//Disallow invalid name
	if ( strcmp( fileName, EMPTY_FILE_NAME ) == 0 )
		return -1;

	//Check if file exists
	file_index_t i;

	for ( i = 0; i < MAX_FILES; i++ )
	{
		File * file = header->files + i;
		if ( file->parentDirectory == current && strcmp( file->fileName, fileName ) == 0 )
		{
			char * data;

			if ( file_loadFromDisk( file, &data ) != 0 )
                return -1;

			out->file = file;
			out->file_data = data;
			out->stream_pointer = 0;
			return 0;
		}
	}

	//Try to create new file
	if ( fs_tryMakeFile( header, fileName, current, &out->file ) != 0 )
		return -1;
	out->stream_pointer = 0;
	out->file_data = NULL;

	fs_flushHeaderToDisk( header );

	return 0;
}

static int fs_tryMkdir( FileSystemHeader * header, const char * directoryName, directory_index_t current )
{
	directory_index_t i;

	directory_index_t searchDir;
    char newName[MAX_NAME_LENGTH + 1];
    if ( fs_getDirAndName( header, directoryName, current, &searchDir, newName ) != 0 )
        return -1;

	//Ensure not attempting to create duplicate directories
	for ( i = 0; i < MAX_DIRECTORIES; i++ )
	{
		Directory * dir = header->directories + i;
		if ( dir->parentDirectory == searchDir && strcmp( dir->directoryName, newName ) == 0 )
			return -1;
	}

	//Try to make the directory
	if ( fs_tryMakeDir( header, newName, searchDir ) != 0 )
        return -1;

    fs_flushHeaderToDisk( header );
    return 0;
}

static int fs_expand( FileSystemHeader * header, File * file, void ** data, size_t additionalBytes )
{
    size_t current_sectors = file->size / FILEBLOCK_DATA_LENGTH + ( file->size % FILEBLOCK_DATA_LENGTH > 0 );
    size_t needed_sectors = ( file->size + additionalBytes ) / FILEBLOCK_DATA_LENGTH +
                            ( ( file->size + additionalBytes ) % FILEBLOCK_DATA_LENGTH > 0 ) - current_sectors;
    size_t available_sectors = bitmask_numFree( &header->bitmask );

    if ( available_sectors < needed_sectors )
        return -1;

    size_t i;
    for ( i = 0; i < needed_sectors; i++ )
    {
        sector_t temp;
        bitmask_getFree( &header->bitmask, &temp );
        bitmask_flip( &header->bitmask, temp.sector );
        temp.in_use = true;
        file_addFileBlock( file, temp );
    }

    if ( *data == NULL )
        *data = malloc( file->size + additionalBytes );
    else
        *data = realloc( *data, file->size + additionalBytes );

    file->size += additionalBytes;

    fs_flushHeaderToDisk( header );

    return 0;
}

static void fs_ls( const FileSystemHeader * header, directory_index_t current, FILE * out )
{
    size_t i;
    for ( i = 0; i < MAX_FILES; i++ )
    {
        if ( !file_isEmpty( header->files + i ) &&
             header->files[i].parentDirectory == current )
            fprintf(out, "%s\n", header->files[i].fileName);
    }

    for ( i = 0; i < MAX_DIRECTORIES; i++ )
    {
        if ( !directory_isEmpty( header->directories + i ) &&
             header->directories[i].parentDirectory == current &&
             i != ROOT_DIRECTORY_INDEX )
            fprintf(out, "%s\n", header->directories[i].directoryName);
    }
}

static int fs_rm( FileSystemHeader * header, directory_index_t current, char * name )
{
    char newName[MAX_NAME_LENGTH + 1];
    directory_index_t searchDirectory;
    if ( fs_getDirAndName( header, name, current, &searchDirectory, newName ) != 0 )
        return -1;

    //Try Remove Directory
    directory_index_t dir;
    for ( dir = 0; dir < MAX_DIRECTORIES; dir++ )
    {
        if ( header->directories[dir].parentDirectory == searchDirectory &&
             strcmp( header->directories[dir].directoryName, newName ) == 0 )
            break;
    }

    if ( dir != MAX_DIRECTORIES )
    {
        directory_initEmpty( header->directories + dir );
        return 0;
    }

    //Try Remove Directory
    file_index_t file;
    for ( file = 0; file < MAX_FILES; file++ )
    {
        if ( header->files[file].parentDirectory == searchDirectory &&
             strcmp( header->files[file].fileName, newName ) == 0 )
            break;
    }

    if ( file != MAX_FILES )
    {
        sector_t temp = header->files[file].firstBlock;

        while ( temp.in_use )
        {
            bitmask_flip( &header->bitmask, temp.sector );

            FileBlock fb;
            while ( SD_read( temp.sector, &fb ) != 0 );
            temp = fb.next;
        }

        file_initEmpty( header->files + file );
        return 0;
    }

    return -1;
}

#endif //BRAD_IS_AWESOME
